/* See the files LICENSE and COPYRIGHT for license and copyright information */
package org.rubiq;

import java.math.BigInteger;

import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

import org.rubiq.reader.DefaultReader;

import org.jruby.Ruby;
import org.jruby.ast.ArrayNode;
import org.jruby.ast.BignumNode;
import org.jruby.ast.FixnumNode;
import org.jruby.ast.FloatNode;
import org.jruby.ast.NilNode;
import org.jruby.ast.Node;
import org.jruby.ast.LocalVarNode;
import org.jruby.ast.RootNode;
import org.jruby.ast.FCallNode;
import org.jruby.ast.CallNode;
import org.jruby.ast.SelfNode;
import org.jruby.ast.StrNode;
import org.jruby.ast.SymbolNode;

import org.jruby.parser.LocalStaticScope;

import org.jruby.runtime.DynamicScope;

import org.jruby.util.ByteList;

import org.jruby.lexer.yacc.ISourcePosition;
import org.jruby.lexer.yacc.SimpleSourcePosition;

/**
 * @author <a href="mailto:ola.bini@gmail.com">Ola Bini</a>
 */
public class RubiqEvaluator {
    private final static ISourcePosition NIL_POS = new SimpleSourcePosition("-", -1);
    private Ruby runtime;
    private LocalStaticScope scope;
    private DynamicScope dscope;

    public RubiqEvaluator(Ruby runtime) {
        this(runtime, new String[0]);
    }

    public RubiqEvaluator(Ruby runtime, String[] vars) {
        this.runtime = runtime;
        this.scope = new LocalStaticScope(null);
        for(int i=0;i<vars.length;i++) {
            this.scope.addVariable(vars[i]);
        }
        this.dscope = new DynamicScope(this.scope);
    }

    public Node transform(LispExpression exp) {
        return new RootNode(NIL_POS, this.dscope, internalTransform(exp, scope));
    }

    private static final boolean isHexChar(char c) {
        return Character.isDigit(c) || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F');
    }

    private Node getInteger(String value, int radix) {
        try {
            return new FixnumNode(NIL_POS, Long.parseLong(value, radix));
        } catch (NumberFormatException e) {
            return new BignumNode(NIL_POS, new BigInteger(value, radix));
        }
    }

    private Node parseNumber(String s) {
        int ix = 0;
        char c = s.charAt(ix++);
        StringBuffer buffer = new StringBuffer();
        if (c == '-') {
        	buffer.append(c);
            c = s.charAt(ix++);
        } else if (c == '+') {
            c = s.charAt(ix++);
        }
        
        char nondigit = 0;

        if (c == '0') {
            int startLen = buffer.length();

            switch (c = s.charAt(ix++)) {
                case 'x' :
                case 'X' : //  hexadecimal
                    c = s.charAt(ix++);
                    if (isHexChar(c)) {
                        for (;ix<s.length(); c = s.charAt(ix++)) {
                            if (c == '_') {
                                if (nondigit != '\0') {
                                    break;
                                }
								nondigit = c;
                            } else if (isHexChar(c)) {
                                nondigit = '\0';
                                buffer.append(c);
                            } else {
                                break;
                            }
                        }
                    }

                    if (buffer.length() == startLen) {
                        return null;
                    } else if (nondigit != '\0') {
                        return null;
                    }
                    if(ix == s.length()) {
                        return getInteger(buffer.toString(),16);
                    } else {
                        return null;
                    }
                case 'b' :
                case 'B' : // binary
                    c = s.charAt(ix++);
                    if (c == '0' || c == '1') {
                        for (;ix<s.length(); c = s.charAt(ix++)) {
                            if (c == '_') {
                                if (nondigit != '\0') {
                                    break;
                                }
								nondigit = c;
                            } else if (c == '0' || c == '1') {
                                nondigit = '\0';
                                buffer.append(c);
                            } else {
                                break;
                            }
                        }
                    }

                    if (buffer.length() == startLen) {
                        return null;
                    } else if (nondigit != '\0') {
                        return null;
                    }
                    if(ix == s.length()) {
                        return getInteger(buffer.toString(),2);
                    } else {
                        return null;
                    }
                case 'd' :
                case 'D' : // decimal
                    c = s.charAt(ix++);
                    if (Character.isDigit(c)) {
                        for (;ix<s.length(); c = s.charAt(ix++)) {
                            if (c == '_') {
                                if (nondigit != '\0') {
                                    break;
                                }
								nondigit = c;
                            } else if (Character.isDigit(c)) {
                                nondigit = '\0';
                                buffer.append(c);
                            } else {
                                break;
                            }
                        }
                    }

                    if (buffer.length() == startLen) {
                        return null;
                    } else if (nondigit != '\0') {
                        return null;
                    }
                    if(ix == s.length()) {
                        return getInteger(buffer.toString(),10);
                    } else {
                        return null;
                    }
                case '0' : case '1' : case '2' : case '3' : case '4' : //Octal
                case '5' : case '6' : case '7' : case '_' : 
                    c = s.charAt(ix++);
                    for (;ix<s.length(); c = s.charAt(ix++)) {
                        if (c == '_') {
                            if (nondigit != '\0') {
                                break;
                            }
							nondigit = c;
                        } else if (c >= '0' && c <= '7') {
                            nondigit = '\0';
                            buffer.append(c);
                        } else {
                            break;
                        }
                    }
                    if (buffer.length() > startLen) {
                        if (nondigit != '\0') {
                            return null;
                        }

                        if(ix == s.length()) {
                            return getInteger(buffer.toString(),8);
                        } else {
                            return null;
                        }
                    }
                case '8' :
                case '9' :
                    throw new RuntimeException("Illegal octal digit.");
                case '.' :
                case 'e' :
                case 'E' :
                	buffer.append('0');
                    break;
                default :
                    if(ix == s.length()) {
                        return new FixnumNode(NIL_POS, 0);
                    } else {
                        return null;
                    }
            }
        }

        boolean seen_point = false;
        boolean seen_e = false;

        for (;; c = s.charAt(ix++)) {
            switch (c) {
                case '0' :
                case '1' :
                case '2' :
                case '3' :
                case '4' :
                case '5' :
                case '6' :
                case '7' :
                case '8' :
                case '9' :
                    nondigit = '\0';
                    buffer.append(c);
                    break;
                case '.' :
                    if (nondigit != '\0') {
                        return null;
                    } else if (seen_point || seen_e) {
                        if(ix == s.length()) {
                            return getNumberToken(buffer.toString(), true, nondigit);
                        } else {
                            return null;
                        }
                    } else {
                        if (ix == s.length() || Character.isDigit((s.charAt(ix)))) {
                            buffer.append('.');
                            if(ix < s.length()) {
                                buffer.append(s.charAt(ix++));
                            }
                            seen_point = true;
                            nondigit = '\0';
                        } else {
                            return null;
                        }
                    }
                    break;
                case 'e' :
                case 'E' :
                    if (nondigit != '\0') {
                        return null;
                    } else if (seen_e) {
                        if(ix == s.length()) {
                            return getNumberToken(buffer.toString(), true, nondigit);
                        } else {
                            return null;
                        }
                    } else {
                        buffer.append(c);
                        seen_e = true;
                        nondigit = c;
                        c = s.charAt(ix++);
                        if (c == '-' || c == '+') {
                            buffer.append(c);
                            nondigit = c;
                        } else {
                            ix--;
                        }
                    }
                    break;
                case '_' : //  '_' in number just ignored
                    if (nondigit != '\0') {
                        return null;
                    }
                    nondigit = c;
                    break;
                default:
                    if(ix == s.length()) {
                        return getNumberToken(buffer.toString(), seen_e || seen_point, nondigit);
                    } else {
                        return null;
                    }
            }
            if(ix == s.length()) {
                return getNumberToken(buffer.toString(), seen_e || seen_point, nondigit);
            }
        }
    }

    private Node getNumberToken(String number, boolean isFloat, char nondigit) {
        if (nondigit != '\0') {
            return null;
        }
        if (isFloat) {
            double d;
            try {
                d = Double.parseDouble(number);
            } catch (NumberFormatException e) {
                d = number.startsWith("-") ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY;
            }
            return new FloatNode(NIL_POS, d);
        }
		return getInteger(number, 10);
    }

    private Node internalTransform(LispExpression exp, LocalStaticScope sscope) {
        if(exp instanceof LispScalar) {
            if(exp instanceof LispString) {
                return new StrNode(NIL_POS, ByteList.create(((LispString)exp).getValue()));
            } else {
                // handle other literals here too, fixnums, globals and so on
                String vv = ((LispScalar)exp).getValue();
                char start = vv.charAt(0);
                if((start >= '0' && start <= '9') || start == '.') {
                    Node n = parseNumber(vv);
                    if(n != null) {
                        return n;
                    }
                }

                if(vv.equals("self")) {
                    return new SelfNode(NIL_POS);
                }
                LocalStaticScope current = sscope;
                int depth = 0;
                int ix=-1;
                while(current != null && (ix = current.isDefined(vv,depth)) == -1) {
                    current = (LocalStaticScope)current.getEnclosingScope();
                }
                if(ix==-1) {
                    throw new RuntimeException("variable " + vv + " is not defined in any lexical scope");
                }
                return new LocalVarNode(NIL_POS, ix, vv);
            }
        } else if(exp instanceof LispCons) {
            LispExpression tag = exp.car();
            if(tag instanceof LispScalar) {
                String name = ((LispScalar)tag).getValue();
                String iname = name.toLowerCase();
                if("quote".equals(iname)) {
                    LispExpression next = exp.cdr().car();
                    if(next instanceof LispCons) {
                        ArrayNode arr = new ArrayNode(NIL_POS);
                        while(next instanceof LispCons) {
                            arr.add(internalTransform(next.car(), sscope));
                            next = next.cdr();
                        }
                        return arr;
                    } else {
                        if(next instanceof LispString) {
                            return new StrNode(NIL_POS, ByteList.create(((LispString)next).getValue()));
                        } else {
                            // also handle the case of numbers here.
                            return new SymbolNode(NIL_POS, ((LispScalar)next).getValue());
                        }
                    }
                } else if("def".equals(iname)) {
                    throw new RuntimeException("Special form '" + name + "' is not yet implemented");
                } else if("lambda".equals(iname)) {
                    throw new RuntimeException("Special form '" + name + "' is not yet implemented");
                } else if("defmacro".equals(iname)) {
                    throw new RuntimeException("Special form '" + name + "' is not yet implemented");
                } else if("class".equals(iname)) {
                    throw new RuntimeException("Special form '" + name + "' is not yet implemented");
                } else if("module".equals(iname)) {
                    throw new RuntimeException("Special form '" + name + "' is not yet implemented");
                // add special handling of operators like +
                // add special handling of read macros
                // add special handling of return and several other special Ruby keywords, break, and so forth.
                // cons, car, cdr
                // possibly make Cons a Ruby type, defining to_a and to_ary.
                // add ; for comments.
                // add ` for macros
                // add set-macro-character.
                // refactor this code into several different implementations
                } else {
                    // check for defined macros here
                    List nodes = new ArrayList();
                    LispExpression next = exp.cdr();
                    while(next instanceof LispCons) {
                        nodes.add(internalTransform(next.car(),sscope));
                        next = next.cdr();
                    }
                    Node selfNode = null;
                    if(nodes.size() > 0) {
                        selfNode = (Node)nodes.remove(0);
                    }
                    ArrayNode args = new ArrayNode(NIL_POS);
                    args.setLightweight(true);
                    for(Iterator iter = nodes.iterator(); iter.hasNext();) {
                        args.add((Node)iter.next());
                    }
                    if(selfNode == null || selfNode instanceof SelfNode) {
                        return new FCallNode(NIL_POS, name, args);
                    } else {
                        return new CallNode(NIL_POS, selfNode, name, args);
                    }

                }
            }
        } else if(exp instanceof LispNil) {
            return new NilNode(NIL_POS);
        }
        return null;
    }

    public static void main(String[] args) throws Exception {
        java.io.FileReader ff = new java.io.FileReader(args[0]);
        DefaultReader def = new DefaultReader(ff);
        LispExpression val;
        Ruby runtime = Ruby.getDefaultInstance();
        RubiqEvaluator evaluator = new RubiqEvaluator(runtime, new String[]{"abc","foo", "gah"});
        while((val = def.readNext()) != null) {
            System.err.println(": " + evaluator.transform(val));
        }
        ff.close();
    }
}// RubiqEvaluator
